/**
 * Copyright (c) 2010-2011 Pierre Labatut
 *
 * This file is part of Space-Chronicles project.
 *
 * Space-Chronicles is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/space-chronicles/>.
 */
package space.game;

import com.jogamp.opengl.util.awt.Screenshot;
import java.awt.Frame;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.opengl.DebugGL3bc;
import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.media.opengl.GL3;
import javax.media.opengl.GL3bc;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.awt.GLCanvas;
import jinngine.collision.SAP2;
import space.model.Material;
import jinngine.math.Matrix4;
import jinngine.math.Quaternion;
import jinngine.math.Vector3;
import jinngine.physics.Body;
import jinngine.physics.DefaultScene;
import jinngine.physics.Scene;
import jinngine.physics.constraint.contact.ContactConstraint;
import jinngine.physics.solver.NonsmoothNonlinearConjugateGradient;
import jinngine.physics.solver.Solver.NCPConstraint;
import space.app.common.ErrorReportEventListener;
import space.debug.GlobalParameters;

import space.game.world.WldInert;
import space.game.world.WldLightCSM;
import space.game.world.WldLightCSMSlice;
import space.game.world.WldScene;
import space.game.world.WldShip;
import space.game.world.WldSkyBox;
import space.gl.Program;
import space.gl.RenderContext;
import space.gl.RenderContext.Pass;
import space.glsl.GLSLDirectionalLight;
import space.glsl.GLSLMaterial;
import space.math.Vector3f;
import space.model.Actor;
import space.model.MdlLibrary;
import space.model.RenderingCamera;
import space.model.StaticCamera;

import space.stream.StructureInput;

/**
 *
 * @author Pierre
 */
public class FwEventListener implements GLEventListener, WldScene {

    /**
     * Configuration
     */
    final Quaternion lightDirection = new Quaternion(.44, .56, -.21, 0.66);
    final GLSLDirectionalLight light = new GLSLDirectionalLight(
            "light",
            new Vector3(1.0f, -.5f, .5f),
            new Vector3f(.3f, .3f, .5f),
            new Vector3f(1.1f, 1.1f, 1.1f));
    final GLSLMaterial material = new GLSLMaterial(
            "material",
            .7f, new Vector3f(.2f, .2f, .2f));
    final boolean record = false;
    int index = 0;
    final File outputDirectory = new File(".\\screenshot");
    final FwEnvironment env;
    final List<Actor> actors = new CopyOnWriteArrayList<Actor>();
    private final InteractiveDesactivationPolicy deactivationPolicy =
            new InteractiveDesactivationPolicy();
    private final Scene scene =
            new DefaultScene(new SAP2(), new NonsmoothNonlinearConjugateGradient(
            45), deactivationPolicy);
    final double timeStep = .01;
    private RenderingCamera camera;
    private WldShip ship;
    private WldSkyBox skybox;
    private WldLightCSM sunlight;
    Program ps, zClamp, additivePS;

    @Override
    public String toString() {
        return "Asteroid Field";
    }

    final public void addActor(Actor actor) {
        actors.add(actor);
        actor.spawn(scene);
    }

    final boolean totalForceSignificant(ContactConstraint contact) {
        double totalforce = 0.;
        for (NCPConstraint ncps : contact) {
            totalforce += ncps.lambda;
        }
        return totalforce > 2.;
    }

    public Map<String, WldMaterial> loadMaterial(GL3bc gl, Map<String, Material> matLib) {
        final Map<String, WldMaterial> wldmap = new HashMap<String, WldMaterial>();
        for (Entry<String, Material> e : matLib.entrySet()) {
            try {
                wldmap.put(e.getKey(),
                        new WldMaterial(gl, e.getValue()));
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
        return wldmap;
    }

    public FwEventListener(FwEnvironment env) {
        this.env = env;
        scene.setTimestep(timeStep);
    }

    public void init(GLAutoDrawable drawable) {

        env.getCtlDevice().poll();
        /**
         * Setup openGLRendering
         */
        final GL3bc gl = buildGL(drawable);
        /**
         * Enable antialiased smooth lies
         */
        /*gl.glEnable(GL3bc.GL_LINE_SMOOTH);
        gl.glEnable(GL3bc.GL_BLEND);
        gl.glHint(GL.GL_LINE_SMOOTH_HINT, GL.GL_NICEST);
        gl.glBlendFunc(GL.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA);
         */

        /*gl.glPolygonOffset(1.f, 1.f);
        gl.glEnable(gl.GL_POLYGON_OFFSET_FILL);*/
        /**
         * Enable Zbuffer
         */
        //  gl.glEnable(GL.GL_DEPTH_TEST);
        //  gl.glDepthFunc(GL.GL_LEQUAL);
        gl.glEnable(gl.GL_CULL_FACE);
        /**
         * Main display settings
         */
        gl.glClearColor(0.5f, 0.0f, 0.0f, 0.0f);    // black background
        gl.setSwapInterval(1); //enable vertical synchronisation

        {
            try {
                skybox = new WldSkyBox(getClass().getResource("/res/nebula.box"));

            } catch (IOException ex) {
                Logger.getLogger(FwEventListener.class.getName()).log(
                        Level.SEVERE, null, ex);
            }

        }
        ps = ShaderProgram.NORMAL_CSM_SHADE.link(gl);
        zClamp = ShaderProgram.Z_CLAMP.link(gl);
        additivePS = ShaderProgram.ENGINE_GLOW.link(gl);
        sunlight = new WldLightCSM(gl);




        for (final Actor a : actors) {
            a.optimize(gl);
        }

        if (true) {
            final Random rand = new Random(6);
            final double size = 2000.;
            final URL libUrl = getClass().getResource(
                    "/res/asteroid_lib.blend.mesh");
            final MdlLibrary lib = StructureInput.readObject(libUrl,
                    MdlLibrary.class);
            final Map<String, WldMaterial> matLib = loadMaterial(gl, lib.getMaterialLib());
            for (String rootName : new String[]{"asteroid.000",
                        "asteroid.001", "asteroid.002", "asteroid.003"}) {

                for (int i = 0; i < 1; i++) {
                    final WldInert inert =
                            new WldInert(lib.getRoot(rootName),
                            lib.getGroupLib(), matLib, "Asteroid");
                    inert.setPosition((rand.nextDouble() - .5) * size,
                            (rand.nextDouble() - .5) * size,
                            (rand.nextDouble() - .5) * size);
                    inert.optimize(gl);
                    inert.optimize(gl);
                    addActor(inert);
                }
            }
        }
        if (true) {
            final MdlLibrary lib = StructureInput.readObject(
                    getClass().getResource("/res/galactica9.blend.mesh"),
                    MdlLibrary.class);


            WldInert pegasus =
                    new WldInert(lib.getRoots().get(0), lib.getGroupLib(), loadMaterial(
                    gl, lib.getMaterialLib()), "Pegasus");
            pegasus.setPosition(300, 300, 0);
            pegasus.optimize(gl);
            pegasus.optimize(gl);
            addActor(pegasus);
        }
        if (true) {
            final MdlLibrary lib = StructureInput.readObject(
                    getClass().getResource("/res/raptor9.blend.mesh"),
                    MdlLibrary.class);
            final Map<String, WldMaterial> matLib = loadMaterial(gl, lib.getMaterialLib());

            ship = new WldShip(lib.getRoot("raptor"), matLib, env.getCtlDevice());
            ship.optimize(gl);
            ship.optimize(gl);
            ship.setPosition(250, -641, -104);
            deactivationPolicy.setAllwaysActive(ship.getBody());
            camera = new RenderingCamera(ship.buildCamera());
            addActor(ship);
        }



        StaticCamera sc1 = new StaticCamera(
                new Matrix4(0.495271562726536, 0.013950179601248449, 0.8686261978800246,
                -202.89065188035704,
                -0.8684382020077488, 0.0342253988576256, 0.4946147100183989, 305.5933513268686,
                -0.022829114042450676, -0.9993167738821005, 0.029065735667748657, -409.744502424489,
                0.0, 0.0, 0.0, 1.0),
                60.0, 10.0, 2000.0);
        StaticCamera sc2 = new StaticCamera(
                new Matrix4(-0.05012758071277598, 0.21239020298991435, 0.9758983693632176,
                -164.0737681115989,
                -0.7486047248099947, -0.6548003161173447, 0.1040553314577265, 464.87163884357824,
                0.6611188937279258, -0.7253460882134323, 0.19181986515852717, -57.27849216036074,
                0.0, 0.0, 0.0, 0.9999999999999999),
                60.0, 10.0, 2000.0);
        StaticCamera sc3 = new StaticCamera(
                new Matrix4(0.7420496908292075, 0.2566070990007553, 0.6192859219154712,
                -709.0588924157071,
                -0.6359510205934155, -0.02263497494238622, 0.771397405567034, 439.02572486112007,
                0.21196357174399497, -0.9662507203887483, 0.14639327034312108, -241.45749715061427,
                0.0, 0.0, 0.0, 1.0000000000000002),
                60.0, 10.0, 2000.0);

        StaticCamera sc4 = new StaticCamera(new Matrix4(-0.21000713970958057, 0.4428765091472213,
                0.8716406363384926, -456.20442064640577,
                0.6046653780975716, -0.6417413690197502, 0.47174971734891114, 509.96840743411883,
                0.7682947232677846, 0.6261217237360704, -0.13302182251288913, -818.9505486296524,
                0.0, 0.0, 0.0, 1.0000000000000002),
                60.0, 10.0, 2000.0);



        StaticCamera sc0 = new StaticCamera(Matrix4.identity(), 60.0, 10.0, 2000.0);

        // camera = new RenderingCamera(sc4);
        final URL libUrl = getClass().getResource("/res/asteroid_lib.blend.mesh");
        final MdlLibrary lib = StructureInput.readObject(libUrl, MdlLibrary.class);
        final Map<String, WldMaterial> matLib = loadMaterial(gl, lib.getMaterialLib());

        final WldInert inert = new WldInert(lib.getRoot("asteroid.000"), lib.getGroupLib(), matLib,
                "Asteroid");
        inert.setPosition(-45, -20, -57 - 5);
        inert.optimize(gl);
        inert.optimize(gl);
        addActor(inert);
        
        

    }

    public void dispose(GLAutoDrawable drawable) {
    }

    public void runGameLogic() {
        try {
            env.getCtlDevice().poll();
        } catch (NoSuchElementException e) {
            env.shutdown();

        }
        for (Actor a : actors) {
            a.update(this, timeStep);
        }


        if (ship != null) {
            ship.update(timeStep);
        }
        scene.tick();
        if (ship != null) {
            ship.updateTrail(timeStep);
        }
    }

    private GL3bc buildGL(GLAutoDrawable drawable) {
        return new DebugGL3bc((GL3bc) drawable.getGL());
    }

    public void draw(GLAutoDrawable drawable) {

        final Matrix4 lightTransformation = lightDirection.toMatrix4(new Matrix4());
        light.setDirection(lightTransformation.multiply(new Vector3(0., 0., -1.)));

        // Common usefull information
        final GL3bc gl = buildGL(drawable);
        final int viewportWidth = drawable.getWidth();
        final int viewportHeight = drawable.getHeight();
        final double aspect = (double) viewportWidth / viewportHeight;

        {   // Render Shadowmap
            final RenderContext rc = new RenderContext(gl, zClamp, light, Matrix4.identity(),
                    Matrix4.identity(), Matrix4.identity(), Pass.SHADOW);
            sunlight.renderShadowMap(rc, lightTransformation, camera, aspect, this);
        }

        {  // Setup viewport
            gl.glViewport(0, 0, viewportWidth, viewportHeight);
            gl.glScissor(0, 0, viewportWidth, viewportHeight);
            gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT);
        }

        { // Setup skybox viewpoint
            final Matrix4 projection = camera.perspective(viewportWidth, viewportHeight);
            final Matrix4 viewpoint = camera.viewpoint();
            viewpoint.a14 = 0;
            viewpoint.a24 = 0;
            viewpoint.a34 = 0;
            final RenderContext rc = new RenderContext(gl, ps, light, projection, viewpoint,
                    Matrix4.identity(), Pass.SHADE);
            gl.glActiveTexture(GL2.GL_TEXTURE2);
            gl.glDisable(GL2.GL_TEXTURE_2D);
            gl.glActiveTexture(GL2.GL_TEXTURE1);
            gl.glDisable(GL2.GL_TEXTURE_2D);
            gl.glActiveTexture(GL2.GL_TEXTURE0);
            rc.stopUsingProgram();
            rc.applyLegacyMatrix();

            skybox.draw(rc.gl());
        }


        final Matrix4 viewProjection = camera.perspective(viewportWidth, viewportHeight);
        final Matrix4 worldView = camera.viewpoint();
        final RenderContext rc = new RenderContext(gl, ps, light, viewProjection, worldView,
                Matrix4.identity(), Pass.SHADE);


        ps.startUsingProgram(gl);
        material.applyUniform(rc);

        gl.glActiveTexture(GL2.GL_TEXTURE0);
        gl.glEnable(GL2.GL_TEXTURE_2D);
        rc.applyUniform1i("diffuseMap", 0);

        gl.glActiveTexture(GL2.GL_TEXTURE1);
        gl.glEnable(GL2.GL_TEXTURE_2D);
        rc.applyUniform1i("normalMap", 1);

        gl.glActiveTexture(GL2.GL_TEXTURE2);
        gl.glEnable(GL2.GL_TEXTURE_2D);
        sunlight.bindTexture(gl);
        rc.applyUniform1i("shadowMap", 2);


        //  sunlight.draw(gl);
        {
            int paramIdx = gl.glGetUniformLocation(ps.programHandle,
                    "shadowMapMatrix");

            final Matrix4 lightMatrices[] = sunlight.getTextureCoordMatrix();
            final float m[] = new float[4 * 4 * 4];
            lightMatrices[0].toFloatArray(m, 0);
            lightMatrices[1].toFloatArray(m, 16);
            lightMatrices[2].toFloatArray(m, 32);
            lightMatrices[3].toFloatArray(m, 48);
            gl.glUniformMatrix4fv(paramIdx, 4, false, m, 0);
        }

        {
            int paramIdx = gl.glGetUniformLocation(ps.programHandle,
                    "splitDistance");
            WldLightCSMSlice[] slices = sunlight.getSlices();
            gl.glUniform4f(paramIdx, (float) slices[0].getNear(),
                    (float) slices[1].getNear(),
                    (float) slices[2].getNear(),
                    (float) slices[3].getNear());
        }

        gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL);
        draw(rc);
        ps.stopUsingProgram(gl);

        final RenderContext additiveRC = new RenderContext(gl, additivePS, light, viewProjection,
                worldView, Matrix4.identity(), Pass.ADDITIVE);

        gl.glBlendFunc(GL3.GL_ONE, GL3.GL_ONE);
        gl.glEnable(GL.GL_BLEND);
        gl.glDepthMask(false);
        gl.glDisable(GL.GL_CULL_FACE);

        additiveRC.startUsingProgram();
        draw(additiveRC);
        additiveRC.stopUsingProgram();

        gl.glDepthMask(true);
        gl.glDisable(GL.GL_BLEND);


        if (GlobalParameters.showShadowMap) {
            sunlight.debugFBO(gl);
        }
        gl.glEnable(gl.GL_DEPTH_TEST);

    }

    public void display(GLAutoDrawable drawable) {

        long gameLogicET = System.nanoTime();
        runGameLogic();
        long renderET = System.nanoTime();
        gameLogicET = renderET - gameLogicET;
        draw(drawable);

        long ts = System.nanoTime();
        renderET = ts - renderET;

        // new StaticCamera(camera).printDeclaration(System.out);

        env.update(ts, renderET, gameLogicET);

        if (record) {
            try {
                Screenshot.writeToFile(
                        new File(outputDirectory, String.format("%05d.jpg",
                        index++)),
                        drawable.getWidth(), drawable.getHeight());
            } catch (GLException ex) {
                Logger.getLogger(FwEventListener.class.getName()).log(
                        Level.SEVERE, null, ex);
            } catch (IOException ex) {
                Logger.getLogger(FwEventListener.class.getName()).log(
                        Level.SEVERE, null, ex);
            }
        }

    }

    public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
    }

    public void draw(RenderContext rc) {
        for (Actor a : actors) {
            a.draw(rc.create());
        }
    }

    void listenToCanvas(GLCanvas canvas) {
        canvas.addGLEventListener(new ErrorReportEventListener(this));
        canvas.addKeyListener(new KeyListener() {

            public void keyTyped(KeyEvent e) {
            }

            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_F5) {
                    final StaticCamera staticCamera = new StaticCamera(camera);
                    staticCamera.printDeclaration(System.out);
                    camera = new RenderingCamera(staticCamera);
                    sunlight.setDebuggerVisible(true);
                }
                if (e.getKeyCode() == KeyEvent.VK_F6) {
                    camera = new RenderingCamera(ship.buildCamera());
                    sunlight.setDebuggerVisible(false);
                }
            }

            public void keyReleased(KeyEvent e) {
            }
        });
    }

    void listenToFrame(Frame frame) {
    }
}
